using UnityEngine;
using System.Collections.Generic;
using System;
using System.Threading.Tasks;
using Function.SAsset;

namespace Util.Pool
{
    public enum BinLoadType
    {
        ASync,
        Sync,
    }

    [System.Serializable]
    public sealed class TrashManRecycleBin
    {
        /// <summary>
        /// Fired when the GameObject was just spawned
        /// </summary>
        public event Action<GameObject> onSpawnedEvent;

        /// <summary>
        /// Fired when the GameObject was just despawned
        /// </summary>
        public event Action<GameObject> onDespawnedEvent;

        /// <summary>
        /// The prefab or GameObject in the scene managed by this class.
        /// </summary>
        public string addressable;

        public BinLoadType binLoadType = BinLoadType.ASync;

        /// <summary>
        /// total number of instances to allocate if one is requested when the bin is empty
        /// </summary>
        public int instancesToAllocateIfEmpty = 1;

        /// <summary>
        /// if true, the recycle bin will not create any more instances when hardLimit is reached and will instead return null for any spanws
        /// </summary>
        public bool imposeHardLimit = false;

        /// <summary>
        /// if imposeHardLimit is true, this will be the maximum number of instances to create
        /// </summary>
        public int hardLimit = 5;

        /// <summary>
        /// if true, any excess instances will be culled at regular intervals
        /// </summary>
        public bool cullExcessPrefabs = false;

        /// <summary>
        /// total instances to keep in the pool. All excess will be culled if cullExcessPrefabs is true
        /// </summary>
        public int instancesToMaintainInPool = 5;

        /// <summary>
        /// how often in seconds should culling occur
        /// </summary>
        public float cullInterval = 10f;

        /// <summary>
        /// if true and Pool.persistBetweenScenes is true this recycle bin will not be purged when a new level loads
        /// </summary>
        public bool persistBetweenScenes = false;


        /// <summary>
        /// stores all of our GameObjects
        /// </summary>
        Stack<GameObject> _gameObjectPool = new Stack<GameObject>();

        /// <summary>
        /// last time culling happened
        /// </summary>
        float _timeOfLastCull = float.MinValue;

        /// <summary>
        /// keeps track of the total number of instances spawned
        /// </summary>
        int _spawnedInstanceCount = 0;


        #region Private

        /// <summary>
        /// allocates
        /// </summary>
        /// <param name="count">Count.</param>
        async Task allocateGameObjects(int count)
        {
            if (imposeHardLimit && _gameObjectPool.Count + count > hardLimit)
                count = hardLimit - _gameObjectPool.Count;

            for (int n = 0; n < count; n++)
            {
                GameObject go = null;
                if (binLoadType == BinLoadType.ASync)
                {
                    go = await AssetManager.InstantiateAwait(addressable);
                }
                else
                {
                    go = AssetManager.Instantiate(addressable);
                }

                go.name = addressable;
                if (go.transform as RectTransform)
                    go.transform.SetParent(Pool.Instance.transform, false);
                else
                    go.transform.parent = Pool.Instance.transform;

                go.SetActive(false);
                _gameObjectPool.Push(go);
            }
        }


        /// <summary>
        /// pops an object off the stack. Returns null if we hit the hardLimit.
        /// </summary>
        async Task<GameObject> pop()
        {
            if (imposeHardLimit && _spawnedInstanceCount >= hardLimit)
                return null;

            if (_gameObjectPool.Count > 0)
            {
                _spawnedInstanceCount++;
                return _gameObjectPool.Pop();
            }

            await allocateGameObjects(instancesToAllocateIfEmpty);
            return await pop();
        }

        #endregion


        #region Public

        /// <summary>
        /// culls any excess objects if necessary
        /// </summary>
        public void cullExcessObjects()
        {
            if (!cullExcessPrefabs || _gameObjectPool.Count <= instancesToMaintainInPool)
                return;

            if (Time.time > _timeOfLastCull + cullInterval)
            {
                _timeOfLastCull = Time.time;
                for (int n = instancesToMaintainInPool; n <= _gameObjectPool.Count; n++)
                    AssetManager.ReleaseInstance(_gameObjectPool.Pop());
            }
        }


        /// <summary>
        /// fetches a new instance from the recycle bin. Returns null if we reached the hardLimit.
        /// </summary>
        public async Task<GameObject> spawn()
        {
            var go = await pop();

            if (go != null)
            {
                if (onSpawnedEvent != null)
                    onSpawnedEvent(go);
            }

            return go;
        }


        /// <summary>
        /// returns an instance to the recycle bin
        /// </summary>
        /// <param name="go">Go.</param>
        public void despawn(GameObject go)
        {
            go.SetActive(false);

            _spawnedInstanceCount--;
            _gameObjectPool.Push(go);

            if (onDespawnedEvent != null)
                onDespawnedEvent(go);
        }


        /// <summary>
        /// clears out the bin optionally calling Destroy on all objects in it. note than any spawned objects are not touched by this operation!
        /// </summary>
        public void clearBin(bool shouldDestroyAllManagedObjects)
        {
            while (_gameObjectPool.Count > 0)
            {
                var go = _gameObjectPool.Pop();

                if (shouldDestroyAllManagedObjects)
                    AssetManager.ReleaseInstance(go);
            }
        }

        #endregion
    }
}